Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/layers/MapLayersManager.kt renovate/fastlane-2.x-lockfile (056eed64) Text, 14.73 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.map.layers

Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787io.ktor.client.HttpClient
Tff7b72import T7ee787io.ktor.client.request.get
Tff7b72import T7ee787io.ktor.client.statement.readRawBytes
Tff7b72import T7ee787io.ktor.http.Url
Tff7b72import T7ee787io.ktor.http.isSuccess
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.asStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.update
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787okio.FileSystem
Tff7b72import T7ee787okio.IOException
Tff7b72import T7ee787okio.Path
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.repository.MapPrefs

T8b949e/**
* Owner of the imported map-layer list, its on-disk persistence, and the import plumbing.
*
* Common code because every host imports layers: both Android flavours and the desktop app observe [mapLayers] and
* render it their own way. This class is the single home for everything that is not the final overlay draw.
*
* Layers persist as files under [mapLayersDirectory]; hidden and network state lives in [MapPrefs]. Nothing here
* touches a platform URI type — an import arrives as a [PickedMapFile] whose bytes the caller already knows how to
* read, which is what lets one implementation serve a ContentResolver and a plain file path alike.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)
Tff7b72class T56d364MapLayersManagerTb4b4b4(
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tff7b72private Tff7b72val Te6edf3httpClientTb4b4b4: Te6edf3HttpClientTb4b4b4,
Tff7b72private Tff7b72val Te6edf3mapPrefsTb4b4b4: Te6edf3MapPrefsTb4b4b4,
T8b949e// Overridable so a test can point the store at a temporary directory. Resolved by the caller rather than inside
T8b949e// the class because on Android it reads a global application context, which a unit test has no reason to hold.
Tff7b72private Tff7b72val Te6edf3layersDirTb4b4b4: Te6edf3Path Tff7b72= Te6edf3mapLayersDirectoryTb4b4b4(Tb4b4b4)Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3fileSystemTb4b4b4: Te6edf3FileSystem Tff7b72= Te6edf3mapLayerFileSystemTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3scope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3dispatchersTb4b4b4.Te6edf3defaultTb4b4b4)

Tff7b72private Tff7b72val Te6edf3_mapLayers Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3MapLayerItemTff7b72>Tff7b72>Tb4b4b4(Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3mapLayersTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ListTff7b72<Te6edf3MapLayerItemTff7b72>Tff7b72> Tff7b72= Te6edf3_mapLayersTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)

Tff7b72init Tb4b4b4{
Te6edf3loadPersistedLayersTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffloadPersistedLayersTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3scopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
T8b949e// await* (not .value) so a cold-start load doesn't see the StateFlow's initial empty default.
Tff7b72val Te6edf3hiddenLayerUrls Tff7b72= Te6edf3mapPrefsTb4b4b4.Te6edf3awaitHiddenLayerUrlsTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3loadedItems Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3fileSystemTb4b4b4.Te6edf3existsTb4b4b4(Te6edf3layersDirTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3fileSystemTb4b4b4.Te6edf3listTb4b4b4(Te6edf3layersDirTb4b4b4)Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3path Tff7b72-Tff7b72>
Tff7b72val Te6edf3metadata Tff7b72= Te6edf3fileSystemTb4b4b4.Te6edf3metadataOrNullTb4b4b4(Te6edf3pathTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3metadataTff7b72?.Te6edf3isRegularFile Tff7b72!Tff7b72= Tff7b72trueTb4b4b4) Tff7b72returnTf0883e@mapNotNull Tff7b72null
Te6edf3resolveLayerTypeTb4b4b4(Te6edf3pathTb4b4b4.Te6edf3nameTb4b4b4.Te6edf3substringAfterLastTb4b4b4(Ta5d6ff'.'Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3ifBlank Tb4b4b4{ Tff7b72null Tb4b4b4}Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3type Tff7b72-Tff7b72>
Tff7b72val Te6edf3uri Tff7b72= Te6edf3pathTb4b4b4.Te6edf3toFileUriTb4b4b4(Tb4b4b4)
Te6edf3MapLayerItemTb4b4b4(
Te6edf3name Tff7b72= Te6edf3displayNameFromFileNameTb4b4b4(Te6edf3pathTb4b4b4.Te6edf3nameTb4b4b4.Te6edf3substringBeforeLastTb4b4b4(Ta5d6ff'.'Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3uri Tff7b72= Te6edf3uriTb4b4b4,
Te6edf3isVisible Tff7b72= Tff7b72!Te6edf3hiddenLayerUrlsTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3uriTb4b4b4)Tb4b4b4,
Te6edf3layerType Tff7b72= Te6edf3typeTb4b4b4,
Te6edf3createdAt Tff7b72= Te6edf3metadataTb4b4b4.Te6edf3lastModifiedAtMillisTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657it Tff7b72> T79c0ff0 Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3emptyListTb4b4b4(Tb4b4b4)
Tb4b4b4}

Te6edf3_mapLayersTb4b4b4.Te6edf3value Tff7b72= Te6edf3loadedItems Tff7b72+ Te6edf3persistedNetworkLayersTb4b4b4(Te6edf3hiddenLayerUrlsTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3_mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3iTb4b4b4(Ta5d6ff"Ta5d6ffLoaded Tffd700${Te6edf3_mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff persisted map layers.Ta5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffError loading persisted map layersTa5d6ff" Tb4b4b4}
Te6edf3_mapLayersTb4b4b4.Te6edf3value Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffpersistedNetworkLayersTb4b4b4(Te6edf3hiddenLayerUrlsTb4b4b4: Te6edf3SetTff7b72<Tffa657StringTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3MapLayerItemTff7b72> Tff7b72=
Te6edf3mapPrefsTb4b4b4.Te6edf3awaitNetworkMapLayersTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3networkString Tff7b72-Tff7b72>
Tff7b72val Te6edf3parts Tff7b72= Te6edf3networkStringTb4b4b4.Te6edf3splitTb4b4b4(Te6edf3NETWORK_LAYER_DELIMITERTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3partsTb4b4b4.Te6edf3size Tff7b72=Tff7b72= Te6edf3NETWORK_LAYER_FIELDSTb4b4b4) Tb4b4b4{
Te6edf3MapLayerItemTb4b4b4(
Te6edf3id Tff7b72= Te6edf3partsTff7b72[T79c0ff0Tff7b72]Tb4b4b4,
Te6edf3name Tff7b72= Te6edf3partsTff7b72[T79c0ff1Tff7b72]Tb4b4b4,
Te6edf3uri Tff7b72= Te6edf3partsTff7b72[T79c0ff2Tff7b72]Tb4b4b4,
Te6edf3isVisible Tff7b72= Tff7b72!Te6edf3hiddenLayerUrlsTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3partsTff7b72[T79c0ff2Tff7b72]Tb4b4b4)Tb4b4b4,
T8b949e// Re-derived from the URL — the stored record carries no type, and this must agree with
T8b949e// what addNetworkMapLayer inferred or a GeoJSON layer reloads as KML and draws nothing.
Te6edf3layerType Tff7b72= Te6edf3networkLayerTypeForTb4b4b4(Te6edf3partsTff7b72[T79c0ff2Tff7b72]Tb4b4b4)Tb4b4b4,
Te6edf3isNetwork Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72null
Tb4b4b4}
Tb4b4b4}

T8b949e/** Import a file the user picked. Unsupported types and unreadable sources are logged and skipped. */
Tff7b72fun Td2a8ffaddMapLayerTb4b4b4(Te6edf3pickedTb4b4b4: Te6edf3PickedMapFileTb4b4b4) Tb4b4b4{
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3layerName Tff7b72= Te6edf3pickedTb4b4b4.Te6edf3displayNameTb4b4b4.Te6edf3substringBeforeLastTb4b4b4(Ta5d6ff'.'Tb4b4b4)Tb4b4b4.Te6edf3ifBlank Tb4b4b4{ Ta5d6ff"Ta5d6ffLayer Tffd700${Te6edf3mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3size Tff7b72+ T79c0ff1Tffd700}Ta5d6ff" Tb4b4b4}
T8b949e// Not split on '.': a MIME subtype like `vnd.google-earth.kml+xml` is matched whole, and taking
T8b949e// the part after its last dot would leave `kml+xml`, which resolves to nothing.
Tff7b72val Te6edf3extension Tff7b72= Te6edf3pickedTb4b4b4.Te6edf3extensionOrMimeTff7b72?.Te6edf3lowercaseTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3layerType Tff7b72= Te6edf3resolveLayerTypeTb4b4b4(Te6edf3extensionTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3layerType Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3wTb4b4b4(Ta5d6ff"Ta5d6ffUnsupported map layer file type: Tffd700$Te6edf3extensionTa5d6ff"Tb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}

Tff7b72val Te6edf3bytes Tff7b72= Te6edf3pickedTb4b4b4.Te6edf3readTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3bytes Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3wTb4b4b4(Ta5d6ff"Ta5d6ffCould not read the picked map layer.Ta5d6ff"Tb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}
Tff7b72val Te6edf3storedUri Tff7b72= Te6edf3writeTb4b4b4(Te6edf3bytesTb4b4b4, Te6edf3layerFileNameTb4b4b4(Te6edf3layerNameTb4b4b4, Te6edf3layerTypeTb4b4b4.Te6edf3storageExtensionTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3storedUri Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{
Tffa657it Tff7b72+ Te6edf3MapLayerItemTb4b4b4(Te6edf3name Tff7b72= Te6edf3layerNameTb4b4b4, Te6edf3uri Tff7b72= Te6edf3storedUriTb4b4b4, Te6edf3layerType Tff7b72= Te6edf3layerTypeTb4b4b4, Te6edf3createdAt Tff7b72= Te6edf3nowMillisTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Import a GeoJSON string (e.g. handed back by the Site Planner headless bridge) as a visible local overlay,
* reusing the same storage-backed layer plumbing as file imports.
*/
Tff7b72fun Td2a8ffaddGeoJsonLayerTb4b4b4(Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4, Te6edf3geoJsonTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3displayName Tff7b72= Te6edf3nameTb4b4b4.Te6edf3ifBlank Tb4b4b4{ Ta5d6ff"Ta5d6ffCoverageTa5d6ff" Tb4b4b4}
Tff7b72val Te6edf3uri Tff7b72= Te6edf3writeTb4b4b4(Te6edf3geoJsonTb4b4b4.Te6edf3encodeToByteArrayTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3layerFileNameTb4b4b4(Te6edf3displayNameTb4b4b4, Te6edf3COVERAGE_EXTENSIONTb4b4b4)Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3uri Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{
Tffa657it Tff7b72+
Te6edf3MapLayerItemTb4b4b4(
Te6edf3name Tff7b72= Te6edf3displayNameTb4b4b4,
Te6edf3uri Tff7b72= Te6edf3uriTb4b4b4,
Te6edf3layerType Tff7b72= Te6edf3LayerTypeTb4b4b4.Te6edf3COVERAGETb4b4b4,
Te6edf3createdAt Tff7b72= Te6edf3nowMillisTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Returns an error message if [name]/[url] are invalid, or null on success. Adds a persisted network layer. */
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4) T8b949e// guard clauses read clearer than nesting for this validation
Tff7b72fun Td2a8ffaddNetworkMapLayerTb4b4b4(Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4, Te6edf3urlTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657String? Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3nameTb4b4b4.Te6edf3isBlankTb4b4b4(Tb4b4b4) Tff7b72|Tff7b72| Te6edf3urlTb4b4b4.Te6edf3isBlankTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return Ta5d6ff"Ta5d6ffInvalid name or URL for network layer.Ta5d6ff"
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3isValidNetworkLayerUrlTb4b4b4(Te6edf3urlTb4b4b4)Tb4b4b4) Tff7b72return Ta5d6ff"Ta5d6ffURL must be a valid http or https URL.Ta5d6ff"

Tff7b72val Te6edf3newItem Tff7b72= Te6edf3MapLayerItemTb4b4b4(Te6edf3name Tff7b72= Te6edf3nameTb4b4b4, Te6edf3uri Tff7b72= Te6edf3urlTb4b4b4, Te6edf3layerType Tff7b72= Te6edf3networkLayerTypeForTb4b4b4(Te6edf3urlTb4b4b4)Tb4b4b4, Te6edf3isNetwork Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{ Tffa657it Tff7b72+ Te6edf3newItem Tb4b4b4}
Tff7b72val Te6edf3encoded Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3newItemTb4b4b4.Te6edf3idTb4b4b4, Te6edf3newItemTb4b4b4.Te6edf3nameTb4b4b4, Te6edf3urlTb4b4b4)Tb4b4b4.Te6edf3joinToStringTb4b4b4(Te6edf3NETWORK_LAYER_DELIMITERTb4b4b4)
Te6edf3mapPrefsTb4b4b4.Te6edf3updateNetworkMapLayers Tb4b4b4{ Tffa657it Tff7b72+ Te6edf3encoded Tb4b4b4}
Tff7b72return Tff7b72null
Tb4b4b4}

Tff7b72fun Td2a8fftoggleLayerVisibilityTb4b4b4(Te6edf3layerIdTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3target Tff7b72= Te6edf3_mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3layerId Tb4b4b4} Tff7b72?: Tff7b72return
Tff7b72val Te6edf3nowVisible Tff7b72= Tff7b72!Te6edf3targetTb4b4b4.Te6edf3isVisible
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{ Te6edf3layers Tff7b72-Tff7b72> Te6edf3layersTb4b4b4.Te6edf3map Tb4b4b4{ Tff7b72if Tb4b4b4(Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3layerIdTb4b4b4) Tffa657itTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3isVisible Tff7b72= Te6edf3nowVisibleTb4b4b4) Tff7b72else Tffa657it Tb4b4b4} Tb4b4b4}

Tff7b72val Te6edf3uri Tff7b72= Te6edf3targetTb4b4b4.Te6edf3uri Tff7b72?: Tff7b72return
Te6edf3mapPrefsTb4b4b4.Te6edf3updateHiddenLayerUrls Tb4b4b4{ Tff7b72if Tb4b4b4(Te6edf3nowVisibleTb4b4b4) Tffa657it Tff7b72- Te6edf3uri Tff7b72else Tffa657it Tff7b72+ Te6edf3uri Tb4b4b4}
Tb4b4b4}

Tff7b72fun Td2a8ffremoveMapLayerTb4b4b4(Te6edf3layerIdTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3layerToRemove Tff7b72= Te6edf3_mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3layerId Tb4b4b4}
Te6edf3layerToRemoveTff7b72?.Te6edf3uriTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3uri Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3layerToRemoveTb4b4b4.Te6edf3isNetworkTb4b4b4) Tb4b4b4{
Te6edf3mapPrefsTb4b4b4.Te6edf3updateNetworkMapLayers Tb4b4b4{ Te6edf3entries Tff7b72-Tff7b72>
Te6edf3entriesTb4b4b4.Te6edf3filterNot Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Tffd700$Te6edf3layerIdTffd700$Te6edf3NETWORK_LAYER_DELIMITERTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3deleteTb4b4b4(Te6edf3uriTb4b4b4)
Tb4b4b4}
Te6edf3mapPrefsTb4b4b4.Te6edf3updateHiddenLayerUrls Tb4b4b4{ Tffa657it Tff7b72- Te6edf3uri Tb4b4b4}
Tb4b4b4}
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{ Te6edf3layers Tff7b72-Tff7b72> Te6edf3layersTb4b4b4.Te6edf3filterNot Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3layerId Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Bump a layer's [MapLayerItem.refreshToken] so renderers re-read it (used for network-layer refresh). */
Tff7b72fun Td2a8ffrefreshMapLayerTb4b4b4(Te6edf3layerIdTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3_mapLayersTb4b4b4.Te6edf3update Tb4b4b4{ Te6edf3layers Tff7b72-Tff7b72>
Te6edf3layersTb4b4b4.Te6edf3map Tb4b4b4{ Tff7b72if Tb4b4b4(Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3layerIdTb4b4b4) Tffa657itTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3refreshToken Tff7b72= Tffa657itTb4b4b4.Te6edf3refreshToken Tff7b72+ T79c0ff1Tb4b4b4) Tff7b72else Tffa657it Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72fun Td2a8ffrefreshAllVisibleNetworkLayersTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3_mapLayersTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNetwork Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3isVisible Tb4b4b4}Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3refreshMapLayerTb4b4b4(Tffa657itTb4b4b4.Te6edf3idTb4b4b4) Tb4b4b4}
Tb4b4b4}

T8b949e/**
* The layer's bytes: fetched for a network layer, read from storage otherwise.
*
* Bytes rather than a stream because a stream has no common type here, and because both readers a layer feeds — the
* KML converter and the zip reader — want the whole document anyway.
*/
Tff7b72suspend Tff7b72fun Td2a8ffreadLayerBytesTb4b4b4(Te6edf3layerItemTb4b4b4: Te6edf3MapLayerItemTb4b4b4)Tb4b4b4: Te6edf3ByteArray? Tb4b4b4{
Tff7b72val Te6edf3uri Tff7b72= Te6edf3layerItemTb4b4b4.Te6edf3uri Tff7b72?: Tff7b72return Tff7b72null
Tff7b72return Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3layerItemTb4b4b4.Te6edf3isNetwork Tff7b72&Tff7b72& Tb4b4b4(Te6edf3uriTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffhttp://Ta5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Te6edf3uriTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffhttps://Ta5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3httpClientTb4b4b4.Te6edf3getTb4b4b4(Te6edf3uriTb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3responseTb4b4b4.Te6edf3statusTb4b4b4.Te6edf3isSuccessTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
T8b949e// Log only the host, not the full URL (paths can carry user-identifying info).
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ffHTTP Tffd700${Te6edf3responseTb4b4b4.Te6edf3statusTffd700}Ta5d6ff fetching network layer from Tffd700${Te6edf3UrlTb4b4b4(Te6edf3uriTb4b4b4)Tb4b4b4.Te6edf3hostTffd700}Ta5d6ff" Tb4b4b4}
Tff7b72returnTf0883e@withContext Tff7b72null
Tb4b4b4}
Te6edf3responseTb4b4b4.Te6edf3readRawBytesTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3fileSystemTb4b4b4.Te6edf3readTb4b4b4(Te6edf3uriTb4b4b4.Te6edf3toLocalPathTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3readByteArrayTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
T8b949e// The caller is a composition-scoped effect, so a layer refresh or leaving the map cancels this fetch
T8b949e// mid-flight. That is not a load failure and must not be reported as one.
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// Redact the URI: it can include a user-chosen file name.
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffError reading map layer (network=Tffd700${Te6edf3layerItemTb4b4b4.Te6edf3isNetworkTffd700}Ta5d6ff)Ta5d6ff" Tb4b4b4}
Tff7b72null
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffwriteTb4b4b4(Te6edf3bytesTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3fileNameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3fileSystemTb4b4b4.Te6edf3createDirectoriesTb4b4b4(Te6edf3layersDirTb4b4b4)
Tff7b72val Te6edf3target Tff7b72= Te6edf3layersDir Tff7b72/ Te6edf3fileName
Te6edf3fileSystemTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3targetTb4b4b4) Tb4b4b4{ Te6edf3writeTb4b4b4(Te6edf3bytesTb4b4b4) Tb4b4b4}
Te6edf3targetTb4b4b4.Te6edf3toFileUriTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IOExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffError writing map layer to storageTa5d6ff" Tb4b4b4}
Tff7b72null
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffdeleteTb4b4b4(Te6edf3uriTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3fileSystemTb4b4b4.Te6edf3deleteTb4b4b4(Te6edf3uriTb4b4b4.Te6edf3toLocalPathTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3mustExist Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3withTagTb4b4b4(Te6edf3TAGTb4b4b4)Tb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffError deleting map layer from storageTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3TAG Tff7b72= Ta5d6ff"Ta5d6ffMapLayersManagerTa5d6ff"
Tff7b72const Tff7b72val Te6edf3NETWORK_LAYER_DELIMITER Tff7b72= Ta5d6ff"Ta5d6ff|:|Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3NETWORK_LAYER_FIELDS Tff7b72= T79c0ff3 T8b949e// id|:|name|:|uri
Tb4b4b4}
Tb4b4b4}

T8b949e/** The directory name under each platform's own app storage. */
Tff7b72internal Tff7b72const Tff7b72val Te6edf3LAYERS_DIR Tff7b72= Ta5d6ff"Ta5d6ffmap_layersTa5d6ff"

T8b949e/**
* Whether [url] is one [MapLayersManager.addNetworkMapLayer] will accept: an explicit, parseable http(s) URL.
*
* The scheme check is on the string, not the parsed protocol — Ktor's [Url] defaults a missing scheme to `http`, so
* `example.com/map.kml` would parse as valid and then be stored as a string nothing can fetch. Shared with the
* add-layer dialog so the form and the store cannot disagree about what is acceptable.
*/
Tff7b72fun Td2a8ffisValidNetworkLayerUrlTb4b4b4(Te6edf3urlTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3hasScheme Tff7b72= Te6edf3urlTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffhttp://Ta5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tff7b72|Tff7b72| Te6edf3urlTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffhttps://Ta5d6ff"Tb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3hasSchemeTb4b4b4) Tff7b72return Tff7b72false
Tff7b72return Tff7b72try Tb4b4b4{
Te6edf3UrlTb4b4b4(Te6edf3urlTb4b4b4)
Tff7b72true
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffSwallowedExceptionTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72false
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* The [LayerType] a network layer's URL implies.
*
* Called both when a layer is added and when a persisted record is loaded, so the two cannot disagree — the stored
* record carries only `id|name|url`. Inference by URL path is all there is to go on without fetching the document; a
* URL that will not parse falls back to KML, matching what an unrecognised extension gets.
*/
Tff7b72internal Tff7b72fun Td2a8ffnetworkLayerTypeForTb4b4b4(Te6edf3urlTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3LayerType Tb4b4b4{
Tff7b72val Te6edf3path Tff7b72=
Tff7b72try Tb4b4b4{
Te6edf3UrlTb4b4b4(Te6edf3urlTb4b4b4)Tb4b4b4.Te6edf3encodedPathTb4b4b4.Te6edf3lowercaseTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffSwallowedExceptionTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72return Te6edf3LayerTypeTb4b4b4.Te6edf3KML
Tb4b4b4}
Tff7b72return Tff7b72if Tb4b4b4(Te6edf3pathTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff.geojsonTa5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Te6edf3pathTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff.jsonTa5d6ff"Tb4b4b4)Tb4b4b4) Te6edf3LayerTypeTb4b4b4.Te6edf3GEOJSON Tff7b72else Te6edf3LayerTypeTb4b4b4.Te6edf3KML
Tb4b4b4}

Served by rngit 1.5.2 - Generated in 0.15s